package migration

import (
	"errors"
	"fmt"
	plumeErrors "gitee.com/lipore/plume/errors"
	"gitee.com/lipore/plume/logger"
	"gorm.io/gorm"
	"hash/fnv"
	"os"
	"regexp"
	"strings"
	"time"
)

type SchemaHistory struct {
	Id            int64 `gorm:"primaryKey"`
	Version       string
	Description   string
	Script        string
	CheckSum      uint64
	Type          string
	InstallOn     time.Time
	ExecutionTime time.Duration
	Success       bool
}

type MigrationOptions struct {
	ScriptFolder string
	Db           *gorm.DB
}

func Migrate(options *MigrationOptions) error {
	db := options.Db
	if db == nil {
		return plumeErrors.WithCode(nil, 0, "db is nil")
	}
	if options.ScriptFolder == "" {
		return plumeErrors.WithCode(nil, 0, "script folder is not set")
	}
	if err := initialSchemaHistoryTable(db); err != nil {
		return err
	}
	if err := migrate(db, options.ScriptFolder); err != nil {
		return err
	}
	return nil
}

func migrate(db *gorm.DB, folder string) error {
	entries, err := os.ReadDir(folder)
	if err != nil {
		return err
	}
	fileNamePattern := `^V(.+)__(.+)\.sql$`
	re := regexp.MustCompile(fileNamePattern)
	for _, entry := range entries {
		if entry.IsDir() {
			continue
		}
		fileName := entry.Name()
		match := re.FindStringSubmatch(fileName)
		if len(match) > 2 {
			version := match[1]
			description := match[2]
			if err := migrateFile(db, folder, fileName, version, description); err != nil {
				return err
			}
		}
	}
	return nil
}

func migrateFile(db *gorm.DB, folder string, name string, version string, description string) error {
	fileContent, err := os.ReadFile(folder + "/" + name)
	if err != nil {
		return err
	}
	hash := fnv.New64()
	hash.Write(fileContent)
	checkSum := hash.Sum64()
	h := &SchemaHistory{}
	err = db.Where("script = ?", name).First(h).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			logger.Infof("migrating %s", name)
			start := time.Now()
			sqls := strings.Split(string(fileContent), ";")
			for _, sql := range sqls {
				if strings.TrimSpace(sql) == "" {
					continue
				}
				e := db.Exec(sql).Error
				if e != nil {
					return e
				}
			}
			end := time.Now()
			h.Version = version
			h.Description = description
			h.Script = name
			h.CheckSum = checkSum
			h.Type = "SQL"
			h.InstallOn = end
			h.ExecutionTime = end.Sub(start)
			h.Success = true
			err = db.Save(h).Error
			if err != nil {
				return err
			}
		} else {
			return err
		}
	} else {
		if h.CheckSum != checkSum {
			logger.Infof("check sum not match, should be %d, but got %d", h.CheckSum, checkSum)
			return plumeErrors.WithCode(nil, 0, "check sum not match")
		}
		logger.Infof("verified %s", name)
	}
	return nil
}

func initialSchemaHistoryTable(db *gorm.DB) error {
	migrator := db.Migrator()
	if !(migrator.HasTable(&SchemaHistory{})) {
		err := migrator.CreateTable(&SchemaHistory{})
		if err != nil {
			return err
		}
		now := time.Now()
		year, month, day := now.Date()
		version := fmt.Sprintf("%d%02d%02d", year, month, day)
		history := &SchemaHistory{
			Version:       version,
			Description:   "<<BaseLine>>",
			Script:        "<<BaseLine>>",
			CheckSum:      0,
			Type:          "BASELINE",
			InstallOn:     now,
			ExecutionTime: 0,
			Success:       true,
		}
		db.Save(history)
	}
	return nil
}
